﻿using System;
using System.Linq;
using System.Collections.Generic;

namespace CleanCode
{
    struct BugetMonth
    {
        public BugetMonth(int year, int month)
        {
            Year = year;
            Month = month;
        }

        public int Year { get; init; }
        public int Month { get; init; }
    }

    class CalculateBuget
    {
        static void Main(string[] args)
        {
            var bugets = new Dictionary<BugetMonth, int>()
            {
                { new BugetMonth(2021, 5), 310 },
                { new BugetMonth(2021, 6), 3000 },
                { new BugetMonth(2021, 7), 620 },
                { new BugetMonth(2021, 8), 930 },
            };

            DateTime startDay;
            DateTime endDay;
            int totalBuget = 0;
            Func<DateTime, DateTime, Dictionary<BugetMonth, int>, int> getBuget = new CalculateBuget().getTotalBuget;
            
            startDay = new DateTime(2021, 06, 28);
            endDay = new DateTime(2021, 07, 21);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 720, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 5, 1);
            endDay = new DateTime(2021, 5, 31);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 310, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 6, 1);
            endDay = new DateTime(2021, 6, 15);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 1500, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 6, 15);
            endDay = new DateTime(2021, 6, 30);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 1600, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 5, 1);
            endDay = new DateTime(2021, 8, 31);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 4860, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 6, 5);
            endDay = new DateTime(2021, 8, 5);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 3370, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 8, 5);
            endDay = new DateTime(2021, 8, 31);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 810, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 4, 5);
            endDay = new DateTime(2021, 4, 30);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 0, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 9, 5);
            endDay = new DateTime(2021, 10, 31);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 0, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 8, 31);
            endDay = new DateTime(2021, 8, 5);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 0, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 4, 5);
            endDay = new DateTime(2021, 5, 31);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 310, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);

            startDay = new DateTime(2021, 8, 5);
            endDay = new DateTime(2021, 9, 30);
            totalBuget = getBuget(startDay, endDay, bugets);
            System.Console.WriteLine("Total buget between {0} and {1} should be: 810, and actual result is: {2}", startDay.ToString("d"), endDay.ToString("d"), totalBuget);
        }

        private int getTotalBuget(DateTime startDay, DateTime endDay, Dictionary<BugetMonth, int> bugets)
        {
            int totalBuget = 0;

            if (endDay < startDay)
            {
                return totalBuget;
            }

            var span = endDay - startDay;
            bool isLessThanOneMonth = ((endDay.Month == startDay.Month) && span.Days < 31);

            int totalDaysInFirstMonth = getDaysInMonth(startDay.Year, startDay.Month);
            int spanDaysInFirstMonth = isLessThanOneMonth ? span.Days + 1 : (totalDaysInFirstMonth - startDay.Day + 1);
            totalBuget += getMonthBuget(new BugetMonth(startDay.Year, startDay.Month), bugets) * spanDaysInFirstMonth / totalDaysInFirstMonth;
            if (isLessThanOneMonth)
            {
                return totalBuget;
            }

            totalBuget += getMonthBuget(new BugetMonth(endDay.Year, endDay.Month), bugets) * endDay.Day / getDaysInMonth(endDay.Year, endDay.Month);

            for (var monthIter = new DateTime(endDay.AddMonths(-1).Year, endDay.AddMonths(-1).Month, 1);
                 monthIter > startDay;
                 monthIter = monthIter.AddMonths(-1))
            {
                totalBuget += getMonthBuget(new BugetMonth(monthIter.Year, monthIter.Month), bugets);
            }

            return totalBuget;
        }

        private int getDaysInMonth(int year, int month)
        {
            return DateTime.DaysInMonth(year, month);
        }

        private int getMonthBuget(BugetMonth month, Dictionary<BugetMonth, int> bugets)
        {
            return bugets.ContainsKey(month) ? bugets[month] : default;
        }

        #region Clean Version

        private int getTotalBugetCleanVersion(DateTime startDay, DateTime endDay, Dictionary<BugetMonth, int> bugets)
        {
            return bugets.Sum(v => v.Value * getOverlapPeriodDays(startDay, endDay, v.Key) / getDaysInMonth(v.Key));
        }

        private int getOverlapPeriodDays(DateTime startDay, DateTime endDay, BugetMonth bugetMonth)
        {
            var monthFirstDay = new DateTime(bugetMonth.Year, bugetMonth.Month, 1);
            var monthLastDay = new DateTime(bugetMonth.Year, bugetMonth.Month, getDaysInMonth(bugetMonth));
            return Math.Max((minDate(endDay, monthLastDay) - maxDate(startDay, monthFirstDay)).Days + 1, 0);
        }

        private int getDaysInMonth(BugetMonth month)
        {
            return DateTime.DaysInMonth(month.Year, month.Month);
        }

        private DateTime maxDate(DateTime first, DateTime second)
        {
            return new DateTime[] { first, second }.Max();
        }

        private DateTime minDate(DateTime first, DateTime second)
        {
            return new DateTime[] { first, second }.Min();
        }

        #endregion
    }
}
